/*
说明：ESP32 接入DeepSeek API，实现AI大模型对话
注意：因为使用非流式输出，获取完全部才输出，自己调整缓冲区和响应长度max_tokens，不然内容太多会响应失败。
Arduino IDE版本为2.3.4， esp32包版本为2.0.17
B站：星汇极客
个人网站获取更多资源：https://www.mqtttwj.online
创建时间：2025.01.28
更新时间：2025.01.28
*/

#include <WiFi.h>                
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>        //记得先安装库

// 配置信息
const char* ssid = "ChinaNet-4tRa";  //wifi名字
const char* password = "88888888";   //wifi密码

const char* host = "api.deepseek.com"; //DeepSeek的网址
const int port = 443;
const char* apiKey = "sk-dd287464e97c467f8b6db8a036561343"; //先注册，创建key

// 分块解析状态机
enum ChunkState { PARSE_CHUNK_SIZE, PARSE_CHUNK_DATA, PARSE_COMPLETE };

//类似main()函数
void setup() {
  Serial.begin(115200);
  WiFi.setAutoReconnect(true);
  connectToWiFi();
}
//类似while()循环
void loop() {
  if (WiFi.status() != WL_CONNECTED) { //重新连接机制
    Serial.println("\n[WARN] WiFi连接丢失，尝试重连...");
    connectToWiFi();
    delay(1000);
    return;
  }

  if (Serial.available() > 0) { //获取串口字符串
    String userInput = Serial.readStringUntil('\n');
    userInput.trim();
    if (userInput.length() > 0) {
      Serial.printf("\n[ESP32] 发送消息：%s\r\n", userInput.c_str());
      String response = sendDeepSeekRequest(userInput);
      if (response.length() > 0) {
        parseApiResponse(response);
      }
    }
    Serial.println("\r\n[DeepSeek] 请输入问题给我（按回车发送）:");
  }
}

// WiFi连接函数
void connectToWiFi() {
  WiFi.mode(WIFI_STA); //STA模式
  Serial.println("\n[ESP32] 正在连接WiFi...");
  WiFi.begin(ssid, password);

  unsigned long startTime = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - startTime < 20000) {
    delay(500);
    Serial.print(".");
  }

  if (WiFi.status() == WL_CONNECTED) {
    Serial.printf("\n[ESP32] 连接成功! IP地址: %s\n", WiFi.localIP().toString().c_str());
    Serial.println("\r\n[DeepSeek] 请输入问题给我（按回车发送）:");
  } else {
    Serial.println("\n[ERROR] 连接超时，请检查配置");
  }
}

// API请求函数
String sendDeepSeekRequest(String userContent) {
  WiFiClientSecure client; // 在这里正确声明客户端
  client.setInsecure();  // 不验证证书
  client.setTimeout(15000); // 设置整体超时时间

  Serial.println("\r\n[ESP32] 正在连接API服务器...");
  if (!client.connect(host, port)) {
    Serial.println("[ERROR] API服务器连接失败");
    return "";
  }

  // 构建JSON请求
  StaticJsonDocument<512> requestDoc;
  requestDoc["model"] = "deepseek-chat"; //deepseek-chat
  requestDoc["max_tokens"] = 5000; // 限制响应长度，注意：这个是一次性返回多少个字，最好多一点，不然获取不完整！！！

  JsonArray messages = requestDoc.createNestedArray("messages");
  JsonObject message = messages.createNestedObject();
  message["role"] = "user";
  message["content"] = userContent;

  String payload;
  serializeJson(requestDoc, payload);

  // 构建HTTP请求
  String request = String("POST /v1/chat/completions HTTP/1.1\r\n") +
                   "Host: " + host + "\r\n" +
                   "Authorization: Bearer " + apiKey + "\r\n" +
                   "Content-Type: application/json\r\n" +
                   "Connection: close\r\n" +
                   "Content-Length: " + payload.length() + "\r\n\r\n" +
                   payload;

  // 发送请求
  Serial.println("[ESP32] 正在发送请求...");
  client.print(request);

  // 跳过响应头
  while (client.connected() && client.available() == 0) delay(10);
  while (client.available()) {
    String line = client.readStringUntil('\n');
    if (line == "\r") break;
  }

  // 处理分块响应
  String responseBody = parseChunkedResponse(client);
  client.stop();
  return responseBody;
}

// 分块响应解析
String parseChunkedResponse(WiFiClientSecure &client) {
  String body;
  ChunkState state = PARSE_CHUNK_SIZE;
  int chunkSize = 0;
  unsigned long lastData = millis();

  Serial.println("[ESP32] 开始接收分块数据...");
  
  while (client.connected() && millis() - lastData < 60000) { // 60秒超时，注意：如果你的内容很多，超时时间要加长，不然还没思考完就超时了！！！
    if (client.available()) {
      lastData = millis();
      
      switch (state) {
        case PARSE_CHUNK_SIZE: {
          String hexLine = client.readStringUntil('\r');
          client.read(); // 跳过\n
          chunkSize = strtol(hexLine.c_str(), NULL, 16);
          Serial.printf("[ESP32] 块大小: %d\n", chunkSize);
          
          if (chunkSize <= 0) {
            state = PARSE_COMPLETE;
            Serial.println("[ESP32] 接收到结束块");
          } else {
            state = PARSE_CHUNK_DATA;
          }
          break;
        }
        
        case PARSE_CHUNK_DATA: {
          Serial.println("[ESP32] 接收块数据中...");
          while (chunkSize > 0) {
            if (client.available()) {
              char c = client.read();
              body += c;
              chunkSize--;
              lastData = millis(); // 每次接收重置超时
            } else {
              delay(10); // 避免CPU占用过高，自己看情况吧
            }
          }
          client.read(); // 跳过\r
          client.read(); // 跳过\n
          state = PARSE_CHUNK_SIZE;
          Serial.println("[ESP32] 块数据接收完成");
          break;
        }
        
        case PARSE_COMPLETE:
          Serial.printf("[ESP32] 接收完成，总长度: %d\n", body.length());
          return body;
      }
    } else {
      delay(50); // 降低CPU占用
    }
  }
  Serial.println("[WARN] 分块接收超时");
  return body;
}

// 响应解析
void parseApiResponse(String response) {
  if (response.length() == 0) {
    Serial.println("[ERROR] 收到空响应");
    return;
  }

  Serial.println("[ESP32] 原始响应数据：");
  Serial.println(response);

  DynamicJsonDocument doc(8192); // 增大缓冲区，注意：这个是esp32能获取的数据，自己看情况！！！
  DeserializationError error = deserializeJson(doc, response);
  
  if (error) {
    Serial.print("[ERROR] JSON解析失败: ");
    Serial.println(error.c_str());
    return;
  }

  if (doc.containsKey("error")) {
    Serial.print("[ERROR] API错误: ");
    serializeJson(doc["error"], Serial);
    return;
  }

  if (doc.containsKey("choices")) {
    const char* content = doc["choices"][0]["message"]["content"];
    Serial.println("\n---------- DeepSeek回复 ----------");
    Serial.println(content);
    
    JsonObject usage = doc["usage"];
    Serial.printf("\nToken使用: 总%d (输入%d + 输出%d)\n",
                  usage["total_tokens"].as<int>(),
                  usage["prompt_tokens"].as<int>(),
                  usage["completion_tokens"].as<int>());
    Serial.println("--------------------------");
  } else {
    Serial.println("[ERROR] 响应格式异常");
  }
}
